package net.shambolica.jport.runner;

import net.shambolica.jport.JPortApplication;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.StringWriter;
import java.net.ConnectException;
import java.util.Properties;

/**
 * <br/>I implement the jport fd/socket protocol:
 * <br/>1. Read properties for Application from stdin, until line containing only 'start' encountered.
 * <br/>2. Start Application (application.start(props,this)).
 * <br/>   If failed startup connection, exit with status 2 ("unable to connect to master").
 * <br/>   If other failure encountered, exit with status 3 ("general init error")..
 * <br/>3a.If application run returns with value >= 0, use that as exit status.
 * <br/>3b.Otherwise, sleep until interrupted (typically by exit(..) from application),
 *         and use exitStatus as the exit status. If interrupted by other than application,
 *         exit status will be 1 ("abnormal termination").
 * <br/><br/>Applications should use STATUS_APPLICATION_BASE as their base exit status (or
 * STATUS_OK if all is well, of course).
 */
public class JPortNetRunner {

    private final static Logger log = LoggerFactory.getLogger(JPortNetRunner.class);

    public static final int STATUS_OK = 0;
    public static final int STATUS_ABNORMAL_TERMINATION = 1;
    public static final int STATUS_CONNECTION_ERROR = 2;
    public static final int STATUS_INITIALIZATION_ERROR = 3;
    public static final int STATUS_APPLICATION_BASE = 5;

    public static final String CONN_PROP_PREFIX = "jport.data.connection";

    private final BufferedReader stdin;
    private final ConnectionFactory connectionFactory;
    private       Properties properties;
    private       DataConnection dataSocket;
    private final JPortApplication application;
    private       volatile Thread thread;
    private       volatile int exitStatus;


    public JPortNetRunner(BufferedReader stdin, JPortApplication application) {
        this(stdin, new ConnectionFactory(), application);
    }
    
    public JPortNetRunner(BufferedReader stdin,
                           ConnectionFactory connectionFactory,
                           JPortApplication application) {
        this.stdin = stdin;
        this.connectionFactory = connectionFactory;
        this.dataSocket = null;
        this.thread = null;
        this.application = application;
        this.exitStatus = STATUS_ABNORMAL_TERMINATION;
        log.info("JPortRunner created (stdin={}, connectionFactory={}): {}", new Object[]{
                stdin, connectionFactory, this});
    }

    /**
     * Configure and run the JPort application.
     * @return an exit status (use this as System.exit(status) in caller).
     */
    public int run() {

        // Start by ensuring only THIS thread is running the jport.
        synchronized (this) {
            if (thread != null) {
                log.error("attempt to run already running jport runner {} by {}", this, Thread.currentThread());
                throw new IllegalStateException("jport already running");
            }
            log.info("started jport runner {} by {}", this, Thread.currentThread());
            thread = Thread.currentThread();
        }

        // Read properties.
        log.info("reading properties from stdin");
        String startLine[] = new String[1];
        try {
            properties = readProperties(stdin, startLine);
        } catch (IOException e) {
            log.error("io exception while reading properties ({})", e.getMessage());
            return STATUS_INITIALIZATION_ERROR;
        } catch (RuntimeException e) {
            log.error("unexpected exception while reading properties", e);
            return STATUS_INITIALIZATION_ERROR;
        }


        // Check that properties are properly terminated by 'start'.
        log.info("got properties: {}", properties);
        if (! startLine[0].equalsIgnoreCase("start")) {

            log.info("no start command received: startLine='{}'", startLine[0]);
            return STATUS_INITIALIZATION_ERROR;

        } else {

            // Obtain connection.
            log.info("starting connection (propbase='{}')", CONN_PROP_PREFIX);
            try {
                dataSocket = connectionFactory.getConnection(properties, CONN_PROP_PREFIX);
            } catch (ConnectException e) {
                log.error("unable to connect ({}: {})", e.getClass().getSimpleName(), e.getMessage());
                return 1;
            } catch (RuntimeException e) {
                log.error("unexpected exception while connecting", e);
                return STATUS_CONNECTION_ERROR;
            }

            // Start application.
            log.info("got connection {}, starting application {}", dataSocket, application);
            try {
                int appReturn = application.start(properties, dataSocket, this);
                if (appReturn >= 0) {
                    log.info("application returned exitStatus={}", appReturn);
                    return appReturn;
                } else {
                    log.info("application returned {}, continuing async", appReturn);
                }
            } catch (RuntimeException e) {
                log.error("unexpected exception while starting application", e);
                return STATUS_INITIALIZATION_ERROR;
            }

            // Wait for external termination request.
            exitStatus = STATUS_ABNORMAL_TERMINATION;
            log.info("application startup complete, now waiting for stop signal");
            try {
                Thread.sleep(Long.MAX_VALUE);
            } catch (Throwable e) {
                log.error("unexpected exception while waiting for stop signal", e);
            }
            log.info("exiting, exitStatus={}", exitStatus);
            return exitStatus;
        }
    }

    /**
     * Read a stream of properties, until non-property line encountered or EOS.
     * @param in The input stream from which to read the properties.
     * @param retLast If not null, and at least 1 entry long, retLast[0] will contain
     * the non-property line that ended parsing (or an empty string if stream ended).
     * @return A set of properties.
     * @throws java.io.IOException if a read error occurs, basically.
     */
    public static Properties readProperties(BufferedReader in, String[] retLast)
            throws IOException {

        Properties ret = new Properties();
        String line;
        String lastLine = null;
        StringWriter tmpWriter = new StringWriter();
        while ((line = in.readLine()) != null) {
            if (line.contains("=")) {
                tmpWriter.append(line).append("\n");
            } else {
                lastLine = line;
                break;
            }
        }

        ret.load(new ByteArrayInputStream(tmpWriter.toString().getBytes()));
        if (retLast != null && retLast.length > 0) {
            retLast[0] = lastLine;
        }

        return ret;
    }



}
